package org.apache.ocean.main.search;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Weight;
import org.apache.lucene.util.PriorityQueue;
import org.apache.ocean.main.IndexSchema;
import org.apache.ocean.main.Index.IndexSnapshot;
import org.apache.ocean.main.search.Search.Sort.Field;
import org.apache.ocean.main.search.cache.FieldCacheKey.FieldIndex;
import org.apache.ocean.main.util.CElement;
import org.apache.ocean.main.util.Constants;
import org.apache.ocean.main.util.Range;
import org.apache.ocean.main.util.Timeout;
import org.apache.ocean.main.util.XMLUtil;
import org.jdom.Element;

public abstract class Search<V> implements Callable<Search.Result<V>> {
	protected final SearchQuery searchQuery;

	public Search(SearchQuery searchQuery) {
		this.searchQuery = searchQuery;
	}

	public static void fillCrowdValues(Crowds crowds, ScoreDoc[] scoreDocs) throws Exception {
		if (crowds != null && crowds.size() > 0) {
			for (ScoreDoc scoreDoc : scoreDocs) {
				Comparable[] crowdValues = new Comparable[crowds.size()];
				for (int x = 0; x < crowds.size(); x++) {
					Crowd crowd = crowds.get(x);
					FieldIndex fieldIndex = scoreDoc.indexSnapshot.getFieldIndex(crowd.getField());
					crowdValues[x] = fieldIndex.getValue(scoreDoc.doc);
				}
				scoreDoc.crowdValues = crowdValues;
			}
		}
	}

	public static Sort toSort(String string) {
		if (StringUtils.isBlank(string))
			return null;
		String[] array = StringUtils.split(string.trim(), ",");
		if (array == null || array.length == 0)
			return null;
		Sort sort = new Sort();
		for (String sortParam : array) {
			String[] paramArray = StringUtils.split(sortParam.trim(), " ");
			if (paramArray == null || paramArray.length == 0)
				continue;
			String field = paramArray[0];
			String order = "asc";
			if (paramArray.length > 1)
				order = paramArray[1];
			boolean reverse = false;
			if (StringUtils.equalsIgnoreCase("asc", order)) {
				reverse = false;
			} else if (StringUtils.equalsIgnoreCase("desc", order)) {
				reverse = true;
			}
			Sort.Field sortField = new Sort.Field(field, reverse);
			sort.add(sortField);
		}
		return sort;
	}

	public static ScoreDoc[] toArray(PriorityQueue priorityQueue) {
		ScoreDoc[] scoreDocs = new ScoreDoc[priorityQueue.size()];
		for (int i = priorityQueue.size() - 1; i >= 0; i--)
			scoreDocs[i] = (ScoreDoc) priorityQueue.pop();
		return scoreDocs;
	}

	public static class Sort extends ArrayList<Field> {
		public final static Sort SCORE_SORT;
		static {
			SCORE_SORT = new Sort();
			SCORE_SORT.add(new Field(Constants.SCORE, true));
		}

		public boolean hasDistance() {
			for (Field field : this) {
				if (field instanceof Distance)
					return true;
			}
			return false;
		}

		public static class Distance extends Field {
			public Distance(boolean reverse) {
				super(null, reverse);
			}
		}

		public static class Field {
			private String name;
			private boolean reverse;

			public Field(String name, boolean reverse) {
				this.name = name;
				this.reverse = reverse;
			}

			public String getName() {
				return name;
			}

			public boolean isReverse() {
				return reverse;
			}
		}
	}

	public static class FieldDoc extends ScoreDoc {
		public Comparable[] comparables;

		public FieldDoc(int doc, float score, IndexSnapshot indexSnapshot, Comparable[] comparables, Comparable[] crowdValues) {
			super(doc, score, crowdValues, indexSnapshot);
			this.comparables = comparables;
		}

		public FieldDoc(int doc, float score, Double distance, IndexSnapshot indexSnapshot, Comparable[] comparables, Comparable[] crowdValues) {
			super(doc, score, distance, crowdValues, indexSnapshot);
			this.comparables = comparables;
		}
	}

	public static class ScoreDoc {
		public int doc;
		public float score;
		public IndexSnapshot indexSnapshot;
		public Double distance;
		public Comparable[] crowdValues;

		public ScoreDoc(int doc, float score, Double distance, Comparable[] crowdValues, IndexSnapshot indexSnapshot) {
			this.doc = doc;
			this.score = score;
			this.distance = distance;
			this.crowdValues = crowdValues;
			this.indexSnapshot = indexSnapshot;
		}
    
		public ScoreDoc(int doc, float score, Comparable[] crowdValues, IndexSnapshot indexSnapshot) {
			this.doc = doc;
			this.score = score;
			this.crowdValues = crowdValues;
			this.indexSnapshot = indexSnapshot;
		}
	}

	public abstract Search.Result<V> call() throws Exception;

	public static class Result<V> {
		private int start;
		private V[] allDocs;
		private int numFound;
		private long elapsed;
		private AggregateResult aggregateResult;

		public Result(int start, V[] allDocs, int numFound, long elapsed) {
			this.start = start;
			this.allDocs = allDocs;
			this.numFound = numFound;
			this.elapsed = elapsed;
		}

		public Result(int start, V[] allDocs, int numFound, AggregateResult aggregateResult, long elapsed) {
			this.start = start;
			this.allDocs = allDocs;
			this.numFound = numFound;
			this.aggregateResult = aggregateResult;
			this.elapsed = elapsed;
		}

		public long getElapsed() {
			return elapsed;
		}

		public void setAggregateResult(AggregateResult aggregateResult) {
			this.aggregateResult = aggregateResult;
		}

		public AggregateResult getAggregateResult() {
			return aggregateResult;
		}

		public List<V> getDocs() {
			List<V> scoreDocs = new ArrayList<V>();
			if (start >= allDocs.length) {
				return scoreDocs;
			} else {
				for (int x = start; x < allDocs.length; x++) {
					scoreDocs.add(allDocs[x]);
				}
			}
			return scoreDocs;
		}

		public int getNumFound() {
			return numFound;
		}

		public V[] getAllDocs() {
			return allDocs;
		}
	}

	public static class Aggregate implements CElement {
		// TODO: change to enum
		public static final String MIN = "min";
		public static final String MAX = "max";
		public static final String AVG = "avg";
		public static final String SUM = "sum";
		public static final String MD5 = "md5";
		private final String type;
		private final String field;
		private final Range range;

		public Aggregate(String type, String field, Range range) {
			this.type = type;
			this.field = field;
			this.range = range;
		}

		public boolean isAvg() {
			return type.equals(AVG);
		}

		public boolean isSum() {
			return type.equals(SUM);
		}

		public boolean isMinMax() {
			return type.equals(Aggregate.MAX) || type.equals(Aggregate.MIN);
		}

		public Element toElement() {
			Element element = new Element("aggregate");
			XMLUtil.setAttribute("field", field, element);
			XMLUtil.createTextElement("range", range.toString(), element);
			return element;
		}

		public String getType() {
			return type;
		}

		public String getField() {
			return field;
		}

		public Range getRange() {
			return range;
		}
	}

	public static class FacetQueries {
		private List<FacetQuery> queries;
		private List<FacetFieldQuery> fields;
		private List<FacetDateQuery> dates;

		public List<FacetQuery> getQueries() {
			return queries;
		}

		public List<FacetFieldQuery> getFields() {
			return fields;
		}

		public List<FacetDateQuery> getDates() {
			return dates;
		}
	}

	public static class FacetFieldQuery {

	}

	public static class FacetQuery {
		private Query query;

		public Query getQuery() {
			return query;
		}
	}

	public static class FacetValue {
		private String name;
		private Integer count;

		public FacetValue(String name, Integer count) {
			this.name = name;
			this.count = count;
		}
	}

	public static class FacetResult {
		private List<FacetValue> facetValues;

		public FacetResult(List<FacetValue> facetValues) {
			this.facetValues = facetValues;
		}
	}

	public static class FacetDateResult extends FacetResult {
		private Date date;
		private String gap;

		public FacetDateResult(List<FacetValue> facetValues, Date date, String gap) {
			super(facetValues);
			this.date = date;
			this.gap = gap;
		}
	}

	public static class FacetDateQuery {
		private String field;
		private String start;
		private String end;
		private String gap;
		private boolean hardEnd;

		public boolean isHardEnd() {
			return hardEnd;
		}

		public String getField() {
			return field;
		}

		public String getStart() {
			return start;
		}

		public String getEnd() {
			return end;
		}

		public String getGap() {
			return gap;
		}
	}

	public static class AggregateResult implements CElement {
		private Object value;
		private Aggregate aggregate;

		public AggregateResult(Object value, Aggregate aggregate) {
			this.value = value;
			this.aggregate = aggregate;
		}

		public AggregateResult(Element element, IndexSchema.Category schemaCategory) {
			String valueText = XMLUtil.getChildText("value", element);
			Aggregate aggregate = new Aggregate(element.getChild("aggregate"));
			value = schemaCategory.getField(aggregate.getField()).getFieldType().stringToObject(valueText);
		}

		public Object getValue() {
			return value;
		}

		public Element toElement() {
			Element element = new Element("aggregateresult");
			XMLUtil.createTextElement("value", value, element);
			element.addContent(aggregate.toElement());
			return element;
		}
	}

	public static class CrowdKey {
		public Comparable value;
		public String field;
		private int hashCode;

		public CrowdKey(String field, Comparable value) {
			this.field = field;
			this.value = value;
			HashCodeBuilder builder = new HashCodeBuilder(333, 1021);
			builder.append(field);
			builder.append(value);
			hashCode = builder.toHashCode();
		}

		public int hashCode() {
      return hashCode;
		}

		public boolean equals(Object object) {
			if (!(object instanceof CrowdKey))
				return false;
			if (object == null)
				return false;
			CrowdKey other = (CrowdKey) object;
			EqualsBuilder equalsBuilder = new EqualsBuilder();
			equalsBuilder.append(field, other.field);
			equalsBuilder.append(value, other.value);
			return equalsBuilder.isEquals();
		}
	}
  
	public static interface CrowdDoc {
		public Comparable[] getCrowdValues();
	}
	
	public static class CrowdValueResult {
		private Map<CrowdKey,CrowdValueResult> map = new HashMap<CrowdKey,CrowdValueResult>();
		private Crowd crowd;
		private Long id;
		private Comparable value;
		private int count = 0;

		public CrowdValueResult(Long id, Crowd crowd, Comparable value) {
			this.id = id;
			this.crowd = crowd;
			this.value = value;
		}
    
		public Collection<CrowdValueResult> getResults() {
			return map.values();
		}
		
		public boolean add(Crowd crowd, Long id, Comparable value) {
			CrowdKey crowdKey = new CrowdKey(crowd.getField(), value);
			CrowdValueResult crowdValueResult = map.get(crowdKey);
			if (crowdValueResult == null) {
				crowdValueResult = new CrowdValueResult(id, crowd, value);
				map.put(crowdKey, crowdValueResult);
			}
			return crowdValueResult.incrementCount();
		}
		
		public boolean incrementCount() {
			count++;
			if (count >= crowd.getMaxCount()) return false;
			return true;
		}
		
		public Crowd getCrowd() {
			return crowd;
		}

		public int getCount() {
			return count;
		}
	}

	public static class Crowd {
		private String field;
		private int maxCount;

		public Crowd(String field, int maxCount) {
			this.field = field;
			this.maxCount = maxCount;
		}

		public String getField() {
			return field;
		}

		public int getMaxCount() {
			return maxCount;
		}
	}

	public static class Crowds extends ArrayList<Crowd> {

	}

	public static class SearchQuery {
		private Weight weight;
		private int start;
		private int rows;
		private Filters filters;
		private Sort sort;
		private Timeout timeout;
		private Aggregate aggregate;
		private FacetQueries facetQueries;
		private Crowds crowds;

		public SearchQuery(Weight weight, int start, int rows, Filters filters, Sort sort, Aggregate aggregate, Timeout timeout) {
			this.weight = weight;
			this.start = start;
			this.rows = rows;
			this.filters = filters;
			this.sort = sort;
			this.aggregate = aggregate;
			this.timeout = timeout;
		}

		public Crowds getCrowds() {
			return crowds;
		}

		public Aggregate getAggregate() {
			return aggregate;
		}

		public boolean hasSort() {
			if (sort != null || sort.size() > 0)
				return true;
			return false;
		}

		public int getNumDocs() {
			return start + rows;
		}

		public Timeout getTimeout() {
			return timeout;
		}

		public Weight getWeight() {
			return weight;
		}

		public int getStart() {
			return start;
		}

		public int getRows() {
			return rows;
		}

		public Filters getFilters() {
			return filters;
		}

		public Sort getSort() {
			return sort;
		}
	}
}
